iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
AI Engineering

30天拆Agent:從Repo看設計系列 第 11 篇

Day 11|用 Codex 做長期 Agent:Memory 要怎麼設定、調整與多人隔離?

  • 分享至 

  • xImage
  •  

前幾天把 Codex App Server 接起來後,我開始遇到一個比 Tool 更實際的問題。

假設我要做的是:

LINE
  ↓
FastAPI
  ↓
Codex App Server
  ↓
Codex Agent

這個 Agent 不是跑完一次就消失,而是會持續跟使用者工作。

例如某天 Codex 說:

我可以把 Jira 的登入 Token 記起來,下次就不用重新登入。

我糾正它:

Credential 不可以放進 Agent Memory,登入資訊要由外層系統管理。

隔幾天我重新開一個 Thread,又問:

上次 LINE Agent 的登入方式最後怎麼設計?

這時我真正想知道的不是「Codex 有沒有 Memory」。

而是三件很實際的事情:

1. 我要在哪裡把 Memory 打開、保留多久?

2. Codex 怎麼決定剛才那句糾正值得記?
   我能不能自己改規則?

3. 如果同一個 Agent 給三個人使用,
   三個人的 Memory 怎麼真的分開?

研究完目前的 openai/codex repo 後,我覺得理解 Codex Memory 最簡單的方法,是先把它分成三層:

層 負責什麼 我能不能直接改
config.toml 開關、保留時間、模型、污染控制 可以
Codex Memory Prompt 什麼值得學、怎麼整理、怎麼讀回來 不能用一般 config 改
Application Layer User、Team、Memory ownership、自訂 Human Gate 自己實作

這三層搞清楚,後面的設計就簡單很多。


第一部分:先把一個人的 Codex Memory 設好

假設現在只有我一個人使用 LINE Agent。

先不要碰多人,也不要先設計新的 Memory DB。

第一件事就是把 Codex 原生 Memory 正確打開。

1. 先改 $CODEX_HOME/config.toml

Codex 的 Memory feature 目前在 feature registry 中已經是 Stable,但預設沒有打開。

所以部署 Agent 時,我不會依賴預設值,而是直接在:

$CODEX_HOME/config.toml

寫清楚:

[features]
memories = true

[memories]
generate_memories = true
use_memories = true
dedicated_tools = true

max_rollout_age_days = 30
min_rollout_idle_hours = 12
max_rollouts_per_startup = 4

max_raw_memories_for_consolidation = 512
max_unused_days = 180

disable_on_external_context = true

第一組是最重要的:

generate_memories = true
use_memories = true

generate_memories 決定:

新的 Thread 要不要被拿去產生 Memory。

use_memories 決定:

新的 Thread 要不要使用以前留下來的 Memory。

所以也可以做出:

generate=true / use=true
→ 正常學習,也正常讀 Memory

generate=false / use=true
→ 不再學新的,但可以使用以前的 Memory

generate=true / use=false
→ 現在不讓舊 Memory 影響 Agent,
  但這次工作仍可以成為未來的 Memory

第二組是 Memory 的生命週期:

max_rollout_age_days = 30
max_unused_days = 180

max_rollout_age_days 是:

多舊的 Thread 還值得第一次拿來學?

Codex預設是 10 天。

如果今天才啟動 Memory,一個半年前、從來沒被整理過的 Thread,通常不會突然被拿去產生新的 Memory。

max_unused_days 則不同,它控制的是:

已經形成的 Memory,多久沒有再被使用後,不再進入 active consolidation?

預設是 30 天。

如果拿 Codex 做 coding assistant,30 天可能夠。

第三組是執行成本:

min_rollout_idle_hours = 12
max_rollouts_per_startup = 4
max_raw_memories_for_consolidation = 512

這是在限制 Codex每次背景整理多少資料,不是 Memory分類規則。


2. 開完 Memory 後,Codex 到底怎麼「學」?

現在回到剛才的例子。

User 說:

Credential 不可以存在 Agent Memory。

Codex不會因為看到「不可以」三個字就寫進資料庫。

它會等 Thread 符合條件後,進入 Memory pipeline:

Thread
  ↓
Phase 1
  ↓
SQLite
  ↓
Phase 2
  ↓
MEMORY.md

Phase 1 與 Phase 2 不需要我們手動呼叫。

它們是 Codex Memory subsystem 自己處理的背景流程。

Phase 1:從一個 Thread 找出值得留下的東西

真正決定「什麼叫值得記?」的是這個檔案:

codex-rs/memories/write/templates/memories/
stage_one_system.md

也就是這份 Markdown 不是讓使用者修改的設定檔。

它是 Codex source code 的一部分,build 時直接被包進 binary。

這份 Prompt 要 Memory Writer 特別注意:

User preference
User correction
Repeated request
Failure
Reusable procedure
Environment / workflow knowledge

例如:

Codex:
可以記住 Jira Token。

User:
不要把 Credential 放進 Memory。

...

Codex:
OneDrive Token 也可以記住。

User:
我前面已經說過,
所有 Credential 都不能進 Memory。

第二次糾正就比一次性的討論更有價值。

Memory Writer可能抽出:

User repeatedly corrected the agent
that external credentials must not be
stored in Agent memory.

Future behavior:
keep credentials in the hosting application.

這就是 Codex目前的 Auto-learning。

它不是 fine-tune 模型。

而是:

User experience
   ↓
Persistent Memory
   ↓
影響未來 Thread

Phase 2 做的是「整理」,不是再學一次

假設 Phase 1 已經產生:

Thread A
→ Credential 不進 Memory

Thread B
→ LINE 透過 FastAPI 接 App Server

Thread C
→ Docker 不可以依賴 sudo

Thread D
→ 再次確認 Credential 規則

這些候選 Memory 先存在 state DB。

Phase 2 才會把它們整理成長期 Memory。

它使用:

codex-rs/memories/write/templates/memories/
consolidation.md

負責:

合併重複內容
處理衝突
淘汰 stale evidence
重新分類
必要時建立 Skill

最後主要產生:

$CODEX_HOME/memories/

├── memory_summary.md
├── MEMORY.md
├── raw_memories.md
├── rollout_summaries/
└── skills/

這也是另一個目前不能直接透過 config 改的地方。


3. 那 Codex 怎麼找到「之前 LINE 的 Memory」?

這部分反而不需要自己做。

Codex原生就有類似 Memory Index 的設計:

memory_summary.md

它不是完整的 Memory,而是幫 Agent 判斷「現在這個 Query 有沒有可能需要歷史記憶?」

例如裡面有:

### LINE Agent

keywords:
line, webhook, fastapi,
codex app server, credential

description:
LINE Agent 架構與登入相關決策。

User問:

之前 LINE 登入最後怎麼設計?

流程大致是:

User Query
   ↓
memory_summary.md

看到 LINE / credential
   ↓
search MEMORY.md
   ↓
找到相關 Task Group
   ↓
真的需要細節時
才讀 rollout_summaries/

讀取規則就在:

codex-rs/ext/memories/templates/memories/
read_path.md

4. 遺忘與 Memory Pollution 也可以直接 config

Memory不是只進不出。

Codex Phase 2 selection 會看:

usage_count
last_usage
source_updated_at

長時間沒再被使用的 Memory,超過:

max_unused_days = 180

後就不再是 active Phase 2 input。

接下來 consolidation 可以根據 workspace diff 把失去有效 evidence 的內容清掉。

所以 max_unused_days 可以理解成:

Memory 的活躍保留窗口。

另一個實用設定是:

[memories]
disable_on_external_context = true

它用來避免 Web Search、MCP 等外部資料直接變成長期 Memory。

這個判斷不是 LLM 做的,而是 Codex runtime 的程式規則。

例如同一個 Codex 有三個 Thread:

Thread A → 修改本地程式
Thread B → Web Search 查 LINE API
Thread C → 解釋本地程式

Codex看到 Thread B 出現 WebSearchCall 這類 external context 時,會取得目前的 thread_id:

state_db::mark_thread_memory_mode_polluted(
    ...,
    sess.thread_id,
    ...
)
.await;

所以最後是:

Thread A → enabled
Thread B → polluted
Thread C → enabled

不是整個 Codex 都被污染,只有使用 external context 的 Thread B。

Phase 1 挑選可學習的 Thread 時,又會直接限制:

WHERE threads.memory_mode = 'enabled'

因此:

Web Search / MCP
        ↓
Runtime 程式規則
        ↓
目前 Thread → polluted
        ↓
不進 Phase 1 自動學習

這裡可以把它理解成兩層:

Thread 能不能被學
→ Runtime 程式判斷

Thread 裡什麼值得記
→ Phase 1 的 LLM 判斷

5. 如果我想自己控制「什麼可以記」,怎麼辦?

到這裡其實可以做一個很明確的選擇。

如果只需要:

Codex自己從工作中學 Preference、Failure、Workflow。

直接使用原生 Memory。

如果想要:

某些重要資訊一定要 User 確認才准寫。

則不要去改 Phase 1。

Codex現在已經有:

add_ad_hoc_note

以及:

extensions/ad_hoc/notes/

這條 Explicit Memory 路徑。

因此可以加一個自己的:

.agents/skills/
└── memory-manager/
    └── SKILL.md

內容只負責:

發現重要 Decision
   ↓
提出建議分類
   ↓
詢問 User
   ↓
User Confirm
   ↓
add_ad_hoc_note

例如:

Agent:

這筆內容建議記成長期 Memory:

「Credential 不進 Agent Memory」

分類建議:Security

是否確認?

User確認後才寫入 ad-hoc note。

如果還擔心模型跳過確認,可以讓真正的 Write Tool要求:

def write_memory(
    category: str,
    content: str,
    confirmed: bool,
):
    if not confirmed:
        raise ValueError(
            "user confirmation required"
        )

    ...

這就是:

Skill
→ 規定 Agent 應該怎麼做

Tool
→ 真正阻止未確認寫入

因為目前 Codex沒有提供一份類似:

MEMORY_RULES.md

讓 Agent Developer 任意重寫 Phase 1 / Phase 2 taxonomy。

這是目前和 Claude Code Skill / Hook 型配置很不一樣的地方。


第二部分:同一個 Agent 給多人用,Memory 怎麼真的分開?

接著把 LINE Agent 從一個人擴成:

Vivian
Peter
Amy

最容易犯的錯是:

Vivian → Thread A
Peter  → Thread B
Amy    → Thread C

然後認為這樣就隔離完成。

Thread確實能隔離 conversation。

但 Codex Memory 還有另一個更重要的 boundary:CODEX_HOME

Codex source code 寫得很清楚:

codex-rs/core/src/config/mod.rs

/// specified by the `CODEX_HOME` environment variable.
/// If not set, defaults to `~/.codex`.
pub fn find_codex_home() ...

而 Memory、config、state 等資料都會以這個 home 為重要根目錄。

SQLite如果沒有另外設定:

sqlite_home
CODEX_SQLITE_HOME

也會 fallback 到:

CODEX_HOME

所以多人 Agent 最簡單、也最容易驗證的做法是:

一個 User 一個 CODEX_HOME。


1. 一個人時怎麼啟?

例如 Vivian:

mkdir -p /data/codex/users/vivian

把前面的:

config.toml

放到:

/data/codex/users/vivian/config.toml

然後:

CODEX_HOME=/data/codex/users/vivian \
codex app-server

這個 App Server process 的 Memory 就會落在自己的 home。

概念上變成:

/data/codex/users/vivian/

├── config.toml
├── memories/
├── state DB
└── other Codex state

2. 多人時,FastAPI 幫每個 User 管自己的 Runtime

現在 Peter 傳 LINE 訊息進來。

FastAPI不是把 Peter 丟進 Vivian 的 App Server Thread。

而是建立:

/data/codex/users/peter

然後用這個 CODEX_HOME 啟另一個 App Server process。

結果就真的會變成:

/data/codex/users/

├── U001/
│   ├── config.toml
│   └── memories/
│
├── U002/
│   ├── config.toml
│   └── memories/
│
└── U003/
    ├── config.toml
    └── memories/

這就是前面一直提到的:

state 分開。

不是概念上的「建立不同 User」。

而是真的讓三個 Codex process 使用三個不同的 state root。


3. 那怎麼還算「同一個 Agent」?

因為共用的是 Agent Definition,不是 Runtime State。

例如所有人都使用:

/opt/line-agent/

├── AGENTS.md
├── .agents/
│   └── skills/
└── shared-config/
    └── config.toml

建立新的 User Runtime 時,只把同一份 config.toml seed 進不同 home。

所以三個人:

共用:

Codex version
AGENTS.md
Skills
Tool definitions
Agent behavior

分開:

Thread
SQLite state
Memory
User-specific config/state

這才是:

1 logical Agent
:
N isolated users

4. LINE Group 如果本來就要共享 Memory 呢?

這時不用另外發明架構。

只要改 tenant key。

私人聊天室:

tenant_id = source["userId"]

群組聊天室:

tenant_id = source["groupId"]

例如:

def get_tenant_id(event):

    source = event["source"]

    if source["type"] == "group":
        return (
            "group-"
            + source["groupId"]
        )

    return (
        "user-"
        + source["userId"]
    )

那:

Vivian private chat

→ /data/codex/users/U001


Peter private chat

→ /data/codex/users/U002


Project LINE Group

→ /data/codex/groups/G001

G001 裡三個人共享同一份 Codex Memory。

這是刻意的 shared memory,而不是意外污染。


5. 目前真正還做不到的是「群組共享 + 個人私有 Memory」

例如希望:

Project 決策
→ Vivian / Peter / Amy 共用

Vivian preference
→ 只有 Vivian

Peter preference
→ 只有 Peter

Codex目前原生 Memory 沒有:

tenant_id
user_id
project_id
memory_scope

這種完整 hierarchy。


References


上一篇
Day 10|Codex App Server:把 Codex Core 接到真正的產品介面
下一篇
Day 12|拆開 Claude Commerce Agents:一個 Agent 到底由哪些模組組成?
系列文
30天拆Agent:從Repo看設計 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言